import { PerspectiveCamera, Vector2, Vector3 } from 'three';
import rodrigo from '../math/rodrigo';
import { latFromMercatorY, mercatorZfromAltitude, earthCircumference } from '@/mercator/MercatorCoordinate';
import { clamp } from '@/utils/utils';
import { ArrayPoint, Coordinate } from '@/typings/GeometryType';
import { CameraOptions } from '@/typings/CameraType';

class Camera {
  private _camera: PerspectiveCamera;
  private _zoom: number = 18;
  private _bearing: number = 0;
  private _pitch: number = 0;
  private _center: Vector2 = new Vector2(0, 0);
  private _width: number;
  private _height: number;
  private _minPitch: number = 0;
  private _maxPitch: number = 85;
  private _fov: number = 36;
  private _pixelsPerMeter: number = 0;

  constructor(width: number, height: number) {
    this._width = width;
    this._height = height;
    this._camera = new PerspectiveCamera(this._fov, width / height, 0.01, 2 ** 13);
    this._calculatePixelsPerMeter();
    this._calculateMatrix();
  }

  resize(width: number, height: number) {
    this._width = width;
    this._height = height;
    this._camera.aspect = width / height;
    this._camera.updateProjectionMatrix();
  }

  private _calculatePixelsPerMeter() {
    const lat = latFromMercatorY(-this._center.y / earthCircumference + 0.5);
    const worldSize = 512 * 2 ** this._zoom;
    this._pixelsPerMeter = mercatorZfromAltitude(1, lat) * worldSize;
  }

  getPixelsPerMeter() {
    return this._pixelsPerMeter;
  }

  set(options: Partial<CameraOptions>) {
    const { zoom, bearing, pitch, center } = options;
    if (typeof zoom === 'number') {
      this._zoom = zoom;
    }
    if (typeof bearing === 'number') {
      this._bearing = bearing;
    }
    if (typeof pitch === 'number') {
      this._pitch = clamp(pitch, this._minPitch, this._maxPitch);
    }
    if (center) {
      this._center.set(center.x, center.y);
    }
    this._calculateMatrix();
  }

  getZoom() {
    return this._zoom;
  }

  getPitch() {
    return this._pitch;
  }

  getBearing() {
    return this._bearing;
  }

  getCenter() {
    return this._center.clone();
  }

  getCamera() {
    return this._camera;
  }

  screenToWorld(point: Coordinate) {
    const x = (point.x / this._width) * 2 - 1;
    const y = -(point.y / this._height) * 2 + 1;
    const vector = new Vector3(x, y, -1);
    vector.unproject(this._camera);
    vector.sub(this._camera.position).normalize();
    const distance = -this._camera.position.z / vector.z;
    const result = this._camera.position.clone();
    result.add(vector.multiplyScalar(distance));
    return result;
  }

  worldToScreen(world: Coordinate) {
    const vector = new Vector3(world.x, world.y, 0);
    const point = vector.project(this._camera);
    const halfWidth = this._width / 2;
    const halfHeight = this._height / 2;
    const x = halfWidth * point.x + halfWidth;
    const y = halfHeight * point.y + halfHeight;
    return new Vector2(x, y);
  }

  /**
    * 根据指定的屏幕坐标和世界坐标设置中心点
    * @param coordinate Coordinate 世界坐标
    * @param point Coordinate 屏幕坐标
    */
  setCenterAtPoint(coordinate: Coordinate, point: Coordinate) {
    const a = this.screenToWorld(point);
    const x = coordinate.x - (a.x - this._center.x);
    const y = coordinate.y - (a.y - this._center.y);
    this._center.set(x, y);
    this._calculateMatrix();
  }

  private _calculateMatrix() {
    const worldHeight = this._height / 2 / this._pixelsPerMeter;
    const distance = worldHeight / Math.tan(this._camera.fov / 2 / 180 * Math.PI);
    if (distance > this._camera.far) {
      this._camera.far += 2 ** 13;
      this._camera.updateProjectionMatrix();
    }
    const { eye, target, up } = Camera.calcEyeTargetUp(this._bearing, this._pitch, distance);
    this._camera.position.set(eye[0] + this._center.x, eye[1] + this._center.y, eye[2]);
    this._camera.up.set(up[0], up[1], up[2]);
    this._camera.lookAt(target[0] + this._center.x, target[1] + this._center.y, target[2]);
  }

  /**
    * 计算视图矩阵所需要的 eye target up
    * @param bearing number 旋转角度
    * @param pitch number 俯仰角度
    * @param focalLength number 焦距
    */
  static calcEyeTargetUp(bearing: number, pitch: number, distance: number) {
    const target: ArrayPoint = [0, 0, 0];

    const p = -pitch / 180 * Math.PI;
    const r = -bearing / 180 * Math.PI;
    const k: ArrayPoint = [0, 0, 1];

    const vy = distance * Math.sin(p);
    const vz = distance * Math.cos(p);
    const eye = rodrigo([0, vy, vz], k, r);
    const up = rodrigo([0, vz, -vy], k, r);
    return { eye, target, up };
  }
}

export default Camera;
